package org.jboss.security.auth.spi;

import java.security.Principal;
import java.security.acl.Group;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.InitialLdapContext;
import javax.security.auth.login.LoginException;
import javax.sql.DataSource;

import org.jboss.security.SimpleGroup;
import org.jboss.security.auth.spi.LdapExtLoginModule;

/**
 * Módulo de autenticação LDAP com roles vindos do banco de dados. Baseado na implementação LdapExtLoginModule do Picketbox 4.0.7.Final.
 * 
 * OBS.: pacote deve ser org.jboss.security.auth.spi devido à segurança em runtime do Jboss (JBossCachedAuthenticationManager).
 *<br>
 *<br>
 * Exemplo de configuração no standalone.xml:
 * 
 * <br>
 * <pre>
 * {@code
 * <security-domain name="CrudJSFRealm"><!-- referência no jboss-web.xml -->
 *                   <authentication>
 *                       <login-module code="org.jboss.security.auth.spi.HULoginModule" flag="required"> <!-- precisa ficar no mesmo pacote de segurança do PicketBox -->
 *                           <module-option name="java.naming.factory.initial" value="com.sun.jndi.ldap.LdapCtxFactory"/>
 *                           <module-option name="java.naming.provider.url" value="ldap://localhost:10389"/> <!-- host e porta do servidor ldap -->
 *                           <module-option name="java.naming.security.authentication" value="simple"/>
 *                           <module-option name="bindDN" value="uid=jboss_server,ou=users,o=mojo"/> <!-- usuário padrão para autenticação do servidor -->
 *                           <module-option name="bindCredential" value="jboss_server"/> <!-- senha do usuário padrão para autenticação do servidor -->
 *                           <module-option name="baseCtxDN" value="ou=users,o=mojo"/> <!-- 'query' de busca dos usuários no ldap -->
 *                           <module-option name="baseFilter" value="(uid=[chave esquerda]0[chave direita];)"/> <!-- substituir pelos caracteres corretos -->
 *                           <module-option name="throwValidateError" value="true"/>
 *                           <module-option name="searchScope" value="ONELEVEL_SCOPE"/>
 *                           <module-option name="dsJndiName" value="java:jboss/datasources/CrudDS"/> <!-- datasource do banco com os roles, deve ser condifurado no jboss -->
 *                           <module-option name="rolesQuery" value="select role, 'Roles' from users u where u.email=?"/> <!-- query de busca dos roles, deve ser alterado -->
 *                       </login-module>
 *                   </authentication>
 *               </security-domain>
 *               }
 *               </pre>
 *               
 * @see org.jboss.security.auth.spi.LdapExtLoginModule
 *
 * @author Leonardo Sampaio
 * @author leonardors@gmail.com
 * @since 16/11/2012 
 *
 */
public class HULoginModule extends LdapExtLoginModule
{

	/**
	 *
	 * Evita a busca de roles pelo LDAP (padrão do LdapExtLoginModule).
	 * 
	 * */
	@Override
	protected void rolesSearch(InitialLdapContext ctx, SearchControls constraints, String user, String userDN,
	         int recursionMax, int nesting) throws NamingException {}

	/**
    * Busca os roles no banco de dados.
    * <br> 
    * <b>public</b> para tornar testável.
    * 
    * @return Group[] grupo de roles do usuário.
	*/
	@Override
	public Group[] getRoleSets() throws LoginException
	{
		List<Group> roles = new ArrayList<Group>();

		String rolesQuery = (String) options.get("rolesQuery");

		if( rolesQuery != null )
		{

			String dsJndiName = (String) options.get("dsJndiName");
			if( dsJndiName == null )
				dsJndiName = "java:/DefaultDS";

			String sessionUserName = getUsername();

			DataSource dataSource = null;
			Context ctx = null;
			Connection dataSourceConnection = null;
			
			try {
				ctx = new InitialContext();
				dataSource = (DataSource) ctx.lookup( dsJndiName );

				PreparedStatement definedPreparedStament = null;

				if (dataSource != null) {
					dataSourceConnection = dataSource.getConnection();

					if(dataSourceConnection != null)  {

						definedPreparedStament = dataSourceConnection.prepareStatement(rolesQuery);

						definedPreparedStament.setString(1, sessionUserName.toLowerCase());

						String roleName = "", groupName = "";
						ResultSet resultSet = definedPreparedStament.executeQuery();

						if( resultSet.next() )
						{
							do
							{
								roleName = resultSet.getString(1);
								
								if (roleName==null)
								{								
									throw new LoginException("Usuário '"+sessionUserName+"' não possui roles cadastrados!");
								}
									
								groupName = resultSet.getString(2);

								if( groupName == null || groupName.length() == 0 )
									groupName = "Roles";
								Group group = new SimpleGroup(groupName);
								try
								{
									Principal p = super.createIdentity(roleName);
									if( trace )
										log.trace("Assign user to role " + roleName);
									group.addMember(p);
									roles.add(group);
								}
								catch(Exception e)
								{
									log.debug("Failed to create principal: "+roleName, e);
								}
							} while( resultSet.next() );
						}
						else {
							
							throw new LoginException("Usuário '"+sessionUserName+"' não possui acesso a este sistema!");
						}
					}
				}
			} catch (NamingException e) {

				throw new LoginException("Erro ao acessar banco através do dsJndiName '"+dsJndiName+"' informado no standalone.xml!");

			} catch (SQLException e) {

				throw new LoginException("Erro ao buscar roles no banco com a consulta '"+rolesQuery+"' informada no standalone.xml!");
			}
			
			finally {
				
				if (ctx !=null && dataSourceConnection!=null) {
					try {
						dataSourceConnection.close();
						dataSource = null;
						ctx.close();
						dataSourceConnection = null;
					} catch (SQLException | NamingException e) {
						e.printStackTrace();
					}
				}
			}
		}

		return roles.toArray(new Group[]{});
	}
}
